package com.atguigu.gmall.activity.service.impl;

import com.atguigu.gmall.activity.mapper.SeckillGoodsMapper;
import com.atguigu.gmall.activity.service.api.SeckillGoodsService;
import com.atguigu.gmall.activity.util.CacheHelper;
import com.atguigu.gmall.common.constant.RedisConst;
import com.atguigu.gmall.common.result.Result;
import com.atguigu.gmall.common.result.ResultCodeEnum;
import com.atguigu.gmall.common.util.MD5;
import com.atguigu.gmall.model.activity.OrderRecode;
import com.atguigu.gmall.model.activity.SeckillGoods;
import com.atguigu.gmall.model.activity.UserRecode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author: 封捷
 * @create-date: 2022/9/18 17:01
 */
@Service
public class SeckillGoodsServiceImpl implements SeckillGoodsService {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private SeckillGoodsMapper seckillGoodsMapper;

    @Override
    public List<SeckillGoods> getSeckillGoodsFromRedis() {
        return redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).values();
    }

    @Override
    public SeckillGoods getSeckillGoodsBySkuId(Long skuId) {
        // ※小心有坑：skuId 是 Long 类型，但是对接 Redis 需要 String 类型
        return (SeckillGoods) redisTemplate.boundHashOps(RedisConst.SECKILL_GOODS).get(skuId.toString());
    }

    @Override
    public void addUserRecodeToQueue(UserRecode userRecode) {
        // 目标一、检查 skuId 对应的商品是否售罄
        // 实现目标的手段：检查状态位是否为 0
        // 状态位为 0：商品售罄
        // 状态位为 1：尚未售罄
        // 1、获取 skuId
        Long skuId = userRecode.getSkuId();

        // 2、根据 skuId 获取状态位数据
        Object statusPosition = CacheHelper.get(skuId.toString());

        // 3、检查状态位
        if ("0".equals(statusPosition)) {
            // 4、商品售罄，流程结束，方法返回
            return ;
        }

        // 目标二、检查用户是否曾经下单
        // 实现目标的手段：尝试执行 setnx 命令
        // 执行成功：“用户已下单”信息保存到了 Redis
        // 执行失败：说明用户曾经下过单
        // 1、获取 userId
        String userId = userRecode.getUserId();

        // 2、拼接执行 setnx 命令时需要使用 key
        String userOrderKey = RedisConst.SECKILL_USER + userId;

        // 3、尝试通过 setnx 命令存入 Redis
        // ※说明：这里我们把 skuId 设置为 value 部分，
        // 表示用户对同一个 skuId 只能下单一次
        Boolean setResult = redisTemplate.opsForValue().setIfAbsent(
                userOrderKey,
                skuId,
                RedisConst.SECKILL__TIMEOUT,
                TimeUnit.SECONDS);

        // 4、判断存入 Redis 的执行结果
        if (!setResult) {
            // 说明发生了重复下单，流程结束
            return ;
        }

        // 目标三、校验商品库存
        // 实现目标手段：把当前 skuId 对应的防超卖队列执行弹出操作
        // 能弹出来：说明还有库存同时在防超卖队列中还减少了一个库存
        // 弹不出来：说明库存已经没有了，商品售罄，此时需要通过 Redis 频道同步修改状态位
        // 1、尝试从防超卖队列弹出元素
        Integer skuIdFromRedis = (Integer) redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).rightPop();

        // 2、检查弹出是否成功
        if (skuIdFromRedis == null) {

            // 同步更新状态位
            redisTemplate.convertAndSend("seckillpush", skuId + ":0");

            // 流程结束
            return ;
        }

        // 目标四、保存临时订单，也就是预下单
        // 实现目标的手段：封装 OrderRecode 对象，并存入 Redis
        // 1、创建 OrderRecode 对象
        OrderRecode orderRecode = new OrderRecode();
        orderRecode.setUserId(userId);

        SeckillGoods seckillGoods = this.getSeckillGoodsBySkuId(skuId);

        orderRecode.setSeckillGoods(seckillGoods);
        orderRecode.setNum(1);

        // ※生成临时订单的订单号
        String orderStr = MD5.encrypt(userId + skuId);

        orderRecode.setOrderStr(orderStr);

        // 2、把 OrderRecode 对象存入 Redis
        redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).put(orderRecode.getUserId(), orderRecode);

        // 目标五、更新 MySQL 数据库中的库存数据
        // 实现目标的手段：执行 SQL 语句
        updateStockCount(skuId, seckillGoods);

    }

    @Override
    public Result checkOrder(Long skuId, String userId) {
        // 提示：
        // RedisConst.SECKILL_ORDERS 对应临时订单（hash类型，field 是 userId，value 是 OrderRecode）
        // RedisConst.SECKILL_ORDERS_USERS 对应正式订单（hash类型，filed 是 orderId，value 是 userId）

        // 一、判断是否进入预下单环节
        // 1、检查当前用户是否进入了抢购队列
        Boolean userQueueingFlag = redisTemplate.hasKey(RedisConst.SECKILL_USER + userId);

        if (userQueueingFlag) {

            // 2、判断是否进入了预下单环节
            Boolean seckillOrderFlag = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).hasKey(userId);

            if (seckillOrderFlag) {
                // 3、如果能够确定已经进入预下单环节，则将 OrderRecode 返回
                OrderRecode orderRecode =
                        (OrderRecode) redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS).get(userId);

                return Result.build(orderRecode, ResultCodeEnum.SECKILL_SUCCESS);
            }
        }

        // 二、判断是否进入正式下单环节
        Boolean orderExists = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).hasKey(userId);

        if (orderExists) {
            // 说明已经正式下单，返回 orderId 和编码
            Object orderId = redisTemplate.boundHashOps(RedisConst.SECKILL_ORDERS_USERS).get(userId);

            return Result.build(orderId, ResultCodeEnum.SECKILL_ORDER_SUCCESS);
        }

        // 三、判断秒杀商品的状态位
        Object statusPosition = CacheHelper.get(skuId.toString());

        if ("0".equals(statusPosition)) {
            return Result.build(null, ResultCodeEnum.SECKILL_FINISH);
        }

        // 四、上面各种情况之外就是正在排队的情况
        return Result.build(null, ResultCodeEnum.SECKILL_RUN);
    }

    private void updateStockCount(Long skuId, SeckillGoods seckillGoods) {
        // 1、获取防超卖队列的长度
        Long currentStockCount = redisTemplate.boundListOps(RedisConst.SECKILL_STOCK_PREFIX + skuId).size();

        // ※由于 MySQL 数据库只是起到数据持久化的作用，主战场在 Redis 中，
        // 所以不必每次都执行保存，可以有一定的间隔
        if (currentStockCount % 2 == 0) {
            // 2、在 seckillGoods 对象中设置最新数据
            seckillGoods.setStockCount(currentStockCount.intValue());

            // 3、执行更新
            seckillGoodsMapper.updateById(seckillGoods);
        }
    }
}
